﻿using System;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using RayDen.Library.Components.Accelleration;
using RayDen.Library.Components.Surface;
using RayDen.Library.Core;
using RayDen.Library.Core.Primitives;
using RayDen.Library.Data.Imaging;
using RayDen.Library.Entity.Interface;

namespace RayDen.SmallPT
{
    public class MCPathIntegrator : BaseRenderer
    {
        public int SamplesPerIteration = 16384>>4;
        public int MaxEyePathDepth = 10;
        public int ValuesPerVertex = 3 + 3 + 2 + 1;

        public float rrProb = .2f;
        public struct PathVertex
        {
            public Vector Wi, Wo;
            public IntersectionInfo Intersection;
            public SurfaceBsdf Bsdf;
            public RgbSpectrum BsdfSample, Emittance;
            public float BsdfPdf;
            public Boolean SpecularBounce;
        }



        public Sample[] samples;
        protected Sampler sampler;


        public class Sample
        {
            public float ImageX, ImageY;
            public float[] Data;

            private int c;

            public float GetValue()
            {
                if (c == Data.Length)
                    c = 0;
                return Data[c++];
            }
        }

        public class Sampler
        {
            private int curX, curY, sampleDepth;
            public int Width, Height, SamplesPerPixel, CurrentPass;

            public Sampler(int w, int h, int spp, int sd)
            {
                Width = w;
                Height = h;
                SamplesPerPixel = spp;
                sampleDepth = sd;
            }

            public Sample GetNextSample(FastRandom rnd)
            {
                curX++;
                if (curX >= Width)
                {
                    curY++;
                    curX = 0;
                    if (curY >= Height)
                    {
                        curY = 0;
                        CurrentPass++;
                    }
                }
                var data = (new float[sampleDepth]).Select(item => rnd.NextFloat()).ToArray();

                return new Sample()
                    {
                        ImageX = curX,
                        ImageY = Height - curY - 1,
                        Data = data
                    };
            }
        }


        public MCPathIntegrator()
        {
            SamplesPerPixel = 1;
            ShadowRayCount = 1;
            CpuThreadsUsed = 8;
            MaxRecursionLevel = 10;
            samples = new Sample[SamplesPerIteration];
        }

        public override void Setup(IRayDenLibraryFrame newFrame)
        {
            base.Setup(newFrame);
            samples = new Sample[SamplesPerIteration];
            sampler = new Sampler(Width, Height, SamplesPerPixel, (MaxRecursionLevel) * (ValuesPerVertex));
        }

        public override string GetCurrentStateInfo()
        {
            return string.Format("{0} Pass:{1}/{2}", base.GetCurrentStateInfo(), this.sampler.CurrentPass, SamplesPerPixel);
        }


        private void ProcessSamples(FilmFrame fframe, Action<IFilmFrame, int> taskCallback)
        {
            bool done = false;
            int iteration = 0;
            var sw = new Stopwatch();
            sw.Start();
            while (!done)
            {

                for (int i = 0; i < SamplesPerIteration; i++)
                {
                    samples[i] = sampler.GetNextSample(rnd.Value);
                }
                Parallel.ForEach(samples, new ParallelOptions() { MaxDegreeOfParallelism = CpuThreadsUsed }, sample =>
                    {
#if VERBOSE
                    try {
#endif
                        IRay ray;

                        camera.GetRay(sample.ImageX, sample.ImageY, out ray);
                        var eyePathradiance = this.EvaluatePath(PropagatePath((RayInfo)ray, MaxRecursionLevel));
                        var offset = (int)(Width * sample.ImageY + sample.ImageX);
                        fframe.Data[offset] += eyePathradiance;
                        fframe.Weight[offset] += 1f;
                        stats.totalSamples = Interlocked.Increment(ref stats.totalSamples);
                        stats.elapsed = sw.Elapsed;

#if VERBOSE
                        }
                        catch(Exception ex) {
                            Console.WriteLine(ex.Message);
                            Console.WriteLine(ex.StackTrace);

                        }
#endif

                    });
                
                taskCallback(fframe, iteration);
                iteration++;
                done = sampler.SamplesPerPixel < sampler.CurrentPass;
            }
            sw.Stop();
        }

        public override void StartRendering(Action<IFilmFrame, int> taskCallback, Action<IFilmFrame> endTaskCallback)
        {
            var frame = new FilmFrame(Width, Height);
            var start = DateTime.UtcNow;
            Console.Write("Rendering :[");

            ProcessSamples(frame, taskCallback);

            stats.elapsed = DateTime.UtcNow - start;
            if (endTaskCallback != null)
                endTaskCallback(frame);
            Console.WriteLine("{0:F9} MRays /sec", (stats.TotalRays / (double)stats.elapsed.Seconds) * 0.000001);
            Console.WriteLine("{0} Shadows samples per path", shadowRayCount);
            Console.WriteLine("{0} Triangles", scene.Geometry.Select(item => item.IndexData.Count / 3).Sum());
        }

        protected PathVertex[] PropagatePath(RayInfo startRay, int maxPaths)
        {
            IntersectionInfo isect;
            var vertices = new PathVertex[maxPaths];
            var ray = new RayInfo(startRay);
            var sm = rnd.Value;
            for (int i = 0; i < maxPaths; i++)
            {
                Interlocked.Increment(ref stats.totalRays);
                if (manager.Intersect(ray, out isect))
                {
                    var vertice = new PathVertex { Wi = -ray.Dir };
                    Interlocked.Increment(ref stats.intersections);
                    var triangle = GetTriangle(isect.PrimitiveId, isect.PrimitiveIndex);
                    if (IsLight(triangle))
                    {
                        var lt = GetLight(triangle.Owner);
                        vertice.Emittance = lt.Le(ray.Dir);
                        vertices[i] = (vertice);
                        break;
                    }
                    vertice.Intersection = isect;
                    var bsdf = matLib.GetSurfMat(isect.Material.Name);
                    vertice.Bsdf = bsdf.First;
                    var t = RgbSpectrum.Unit;
                    vertice.BsdfSample = bsdf.Sample_f(ref vertice.Wi, out vertice.Wo, ref isect.GeometryInfo.GeoNormal,
                                                       ref isect.GeometryInfo.ShadingNormal, ref t,
                                                       sm.NextFloat(),
                                                       sm.NextFloat(),
                                                       sm.NextFloat(), ref isect.TextureData    , out vertice.BsdfPdf,
                                                       out vertice.SpecularBounce);

                    vertices[i] = (vertice);

                    ray = new RayInfo(isect.GeometryInfo.HitPoint, vertice.Wo);

                }
                else
                {
                    vertices[i] = (new PathVertex() { Wi = -ray.Dir });
                    break;
                }
            }
            return vertices;
        }

        protected RgbSpectrum EvaluatePath(PathVertex[] path)
        {
            RgbSpectrum Ld = new RgbSpectrum(0f), throughPut = new RgbSpectrum(1f);
            for (int i = 0; i < path.Length; i++)
            {
                if (path[i].Intersection != null)
                {
                    if (path[i].Bsdf.IsDiffuse())
                    {
                        var surfaceColor = path[i].Intersection.Material.DiffuseReflectance.Average > path[i].Intersection.Material.GlossReflectance.Average ? path[i].Intersection.Material.DiffuseReflectance : path[i].Intersection.Material.GlossReflectance;
                        var diffRadiance = new RgbSpectrum();
                        float srd = 0f;
                        for (int j = 0; j < shadowRayCount; j++)
                        {
                            var lightSample = Integrator.UniformSampleLight(ref path[i].Wi, this.manager, this.scene,
                                                                            this.lights, path[i].Intersection, path[i].Bsdf,
                                                                            rnd.Value);
                            if (!lightSample.IsBlack())
                            {
                                diffRadiance += surfaceColor * lightSample * throughPut;
                                srd += 1f;
                            }
                        }
                        Ld += diffRadiance;
                        /* 
                        float lightPdf, lspdf;
                        RayInfo shadowRay;

                        var light = lights[(int) (rnd.Value.NextFloat()*lights.Count)];
                        lspdf = shadowRayCount/(float) lights.Count;

                        var lightSample = light.Sample(ref path[i].Intersection.Point, ref path[i].Intersection.Normal,
                                                       rnd.Value.NextFloat(), rnd.Value.NextFloat(), out shadowRay,
                                                       out lightPdf);
                        if( !lightSample.IsBlack() && !manager.Intersect(shadowRay) ) {
                            var lwi = shadowRay.Dir;
                            var lpt = throughPut*surfaceColor;
                            lightSample *= lpt * Vector.Dot(ref path[i].Intersection.Normal, ref lwi) *
                                                               path[i].Bsdf.f(ref lwi, ref path[i].Wo, ref path[i].Intersection.ShadingNormal);
                            Ld += lightSample/(lightPdf*lspdf);
                        }
                         */
                    }
                    if (path[i].BsdfPdf >= MathLab.Epsilon && !path[i].BsdfSample.IsBlack())
                    {
                        throughPut *= path[i].BsdfSample / path[i].BsdfPdf;
                    }
                    //else {
                    //    Ld += throughPut;
                    //    break;
                    //}
                }
                else
                    if (!path[i].Emittance.IsBlack())
                    {
                        Ld += throughPut * path[i].Emittance;
                        break;
                    }
                    else
                    {
                        Ld += throughPut * this.SampleEnvironment(ref path[i].Wi);
                        break;
                    }
            }
            return Ld / (float)path.Length;
        }

        protected RgbSpectrum SampleEnvironment(ref Vector vector)
        {
            if (EnvironmentMap != null)
            {
                return EnvironmentMap.Le(vector);
            }
            return RgbSpectrum.ZeroSpectrum();
        }
    }
}